Chapter 09 / Caching

Caching

Memahami mekanisme yang membuat sistem backend berperforma tinggi jadi cepat, dari Google Search sampai Netflix dan Redis.


Versi Sederhana

01 / DEFINITION

Apa Itu Caching?

Caching is a mechanism using which we decrease the amount of time and effort it takes to perform some work.

Satu kalimat itu seluruh konsepnya diringkas. Tapi mari kita buka dengan benar karena implikasinya merembes ke seluruh engineering backend.

Definisi Dua Bagian

Ada dua cara memahami caching, versi bahasa polos dan versi teknis. Keduanya mengatakan hal yang sama persis, hanya pada tingkat presisi berbeda:

Plain English Definition

Caching adalah mekanisme yang kita pakai untuk mengurangi jumlah waktu dan usaha yang dibutuhkan melakukan sejumlah pekerjaan. Itulah penjelasan daring tentang apa sebenarnya caching.

Technical Definition

Caching adalah menyimpan sebagian (subset) dari suatu data: anggap kita punya sumber data primer, saat kita menyimpan subset (bukan seluruh data, sebagian) dari data itu, tergantung kegunaan datanya, frekuensi pemakaiannya, probabilitas pemakaian berikutnya, waktu, dll., tergantung banyak parameter, kita menyimpan subset itu di lokasi yang lebih cepat diakses, yang memakan waktu lebih sedikit, dan juga usaha lebih sedikit. Jadi secara teknis, caching adalah mekanisme yang bisa kita pakai untuk mengurangi jumlah waktu dan usaha yang dibutuhkan mengambil atau melakukan semacam operasi.

Kenapa "Subset" Kata Terpenting

Definisinya bilang subset, bukan semua data, bukan salinan semuanya, melainkan bagian data primer yang dipilih dengan hati-hati. Ini kritis karena dua alasan:

  • Caching lebih mahal daripada penyimpanan disk. Menyimpan data di cache (biasanya di RAM) jauh lebih mahal per gigabyte daripada menyimpan di hard disk atau SSD. Anda tidak mungkin mampu men-cache semuanya. Penyimpanan disk relatif lebih murah dan menawarkan banyak kapasitas, sebab itu kita tidak menaruh semuanya di memori cache.
  • Kapasitas cache terbatas. RAM langka dibanding penyimpanan sekunder. Server khas mungkin punya 64GB atau 128GB RAM tapi beberapa terabyte disk. Anda harus selektif.
  • Men-cache semuanya meniadakan tujuannya. Jika Anda men-cache semuanya, Anda cuma menduplikasi penyimpanan dengan biaya jauh lebih tinggi. Intinya men-cache hal yang tepat, data yang benar-benar akan diakses lagi segera.

Parameter yang Mengatur Apa yang Di-cache

Saat merancang lapisan caching, inilah parameter yang Anda evaluasi untuk memutuskan apa yang layak masuk cache:

  • Frekuensi pemakaian: Seberapa sering data ini diakses? Data yang diakses ribuan kali per menit kandidat jauh lebih baik daripada data yang diakses sekali sehari.
  • Probabilitas pemakaian berikutnya: Berdasarkan pola akses, seberapa besar kemungkinan statistik data ini diminta lagi segera? Model machine learning dipakai di skala besar (Netflix, Twitter) untuk memprediksi ini.
  • Kebaruan (recency): Apakah data ini baru diakses? Data yang baru diakses secara statistik lebih mungkin diakses lagi segera (prinsip temporal locality).
  • Biaya komputasi ulang: Seberapa mahal membuat ulang data ini jika tidak ada di cache? Pencarian DB sederhana murah. Komputasi ML trending-topics atas miliaran tweet sangat mahal. Semakin mahal komputasinya, semakin kuat alasan men-cache.
  • Volatilitas data: Seberapa sering data ini berubah? Data yang sangat volatil (mis. harga saham langsung) kandidat caching yang buruk karena langsung basi. Data statis (deskripsi produk, profil pengguna) kandidat sangat baik.

Kenapa Caching Penting: Konteks Performa Tinggi

Satu mekanisme ini, caching, adalah faktor besar di banyak aplikasi berperforma tinggi. Saat kita bilang performa tinggi, maksudnya aplikasi yang melacak latensi dalam hitungan mikro detik dua digit atau milidetik. Di skala itu, selisih 5ms di waktu respons pun terasa, dan selisih 50ms tidak bisa diterima.

Tanpa caching, sistem lalu lintas tinggi menghadapi dua hambatan mustahil:

The Two Core Scenarios Where Caching is Essential

1. Komputasi berat: Saat menghasilkan hasil butuh sumber daya CPU, GPU, atau memori signifikan (inferensi ML, join kompleks lintas jutaan baris, pipeline agregasi). Anda tidak ingin mengulang ini untuk setiap request pengguna. 2. Transfer data berat: Saat data yang dikirim besar (file video, pustaka gambar, payload JSON besar) dan mengirimnya lewat jaringan untuk setiap request lambat dan mahal. Anda ingin data itu sudah dekat dengan pengguna.

Dua skenario ini, menghindari komputasi ulang yang mahal dan menghindari transfer data berat yang redundan, adalah pola yang akan Anda kenali di setiap kasus caching yang Anda temui sepanjang karier. Kapan pun Anda melihat salah satu dari dua situasi ini, insting pertama Anda harus: "Bisa kita cache ini?"


02 / REAL-WORLD EXAMPLES

Contoh Dunia Nyata

Sebelum menyelami mekanikanya, membantu memahami kenapa caching penting di skala produksi. Mari lihat tiga contoh yang mengilustrasikan konsep dan dua skenario inti tempat caching selalu muncul. Setelah contoh-contoh ini, Anda akan mulai melihat polanya di mana-mana.

Contoh 1: Google Search

Hampir semua kita memakai atau pernah memakai Google Search di browser. Apa yang sebenarnya terjadi saat Anda mengetik sesuatu di bar pencarian Google dan menekan Enter?

Query itu diproses mesin pencari Google lewat algoritma dan alur kerja yang cukup rumit. Setiap query melewati pipeline yang biasanya melibatkan:

  • Crawling: bot Google terus merayapi web, menemukan dan mengunduh miliaran halaman
  • Indexing: konten yang dirayapi dianalisis dan ditambahkan ke index pencarian raksasa
  • Ranking: saat query tiba, ratusan algoritma ranking menentukan hasil mana yang paling relevan

Seluruh proses ini mahal secara komputasi: saat kita bilang mahal, maksudnya mahal secara komputasi. Ia butuh banyak daya komputasi, banyak CPU, banyak sumber daya memori, dll.

Sekarang anggap query seperti "what is the weather today", query seperti ini dicari jutaan dan jutaan kali setiap hari. Tanpa caching, tanpa mengimplementasikan mekanisme bernama caching ini, server Google harus menghitung ulang semua hasil untuk setiap query. Setiap query yang melibatkan cuaca saat ini sebuah lokasi akan menuntut melewati seluruh index, menjalankan semua algoritma ranking, dan mengambil hasilnya, yang pada gilirannya memperlambat waktu respons secara signifikan dan menimbulkan beban server sangat tinggi.

Apa yang Dilakukan Google Sebagai Gantinya

Google memakai sistem caching terdistribusi di memori untuk menyimpan hasilnya. Kata kuncinya di sini terdistribusi, server sistem caching tersebar di seluruh dunia. Mereka tidak hanya terkonsentrasi di satu lokasi tapi tersebar di seluruh dunia, dan server cache itu menyimpan hasil, hasil apa pun yang dikembalikan semua algoritma ranking dan berbagai algoritma yang terlibat di seluruh alur kerja Google Search. Hasil itu di-cache atau disimpan di server-server ini.

Saat pengguna mencari, sistem pertama memeriksa apakah hasil query tertentu itu ada di cache atau tidak:

Google Search cache flow with detailed steps User Query "weather in Delhi" Distributed Cache in-memory store global servers CACHE HIT , found it! Return instantly very fast retrieval CACHE MISS, not found Full pipeline runs crawl -> rank -> results store in cache
Fig 1.1: Google Search: Cache Hit vs Cache Miss flow with result storage loop

Mari telusuri kedua jalur secara presisi:

  • Cache Hit: Jika query sudah pernah dicari (mis. "weather in Delhi" dicari 5 menit lalu oleh orang lain), sistem menemukannya di cache dan mengembalikan hasilnya seketika. Mengambil data dari cache sangat cepat, itulah salah satu alasan utama kita memakai cache.
  • Cache Miss: Jika query tidak pernah dicari, atau cache kedaluwarsa, sistem melewati alur kerja penuh normal, semua ranking, semua sorting, algoritma apa pun yang terlibat. Lalu ia mengambil hasil itu, men-cache-nya, dan mengembalikannya ke pengguna. Jadi lain kali pengguna yang sama atau pengguna lain mengetik query yang sama, hasilnya bisa diambil langsung dari cache.
Key Vocabulary Introduced

Cache Hit: Saat Anda mencari data di cache dan menemukannya. Jalur cepat. Tanpa komputasi ulang. Cache Miss: Saat Anda mencari data di cache dan ia tidak ada. Harus ke sumber primer, menghitung hasilnya, lalu menyimpannya untuk pemakaian masa depan.

Contoh 2: Netflix & CDN

Netflix platform streaming besar dan global yang mengirimkan berbagai jenis konten, film, serial, anime, dll., ke jutaan pengguna di seluruh dunia. Ia men-stream volume data besar, dan saat kita bilang besar, bisa beberapa terabyte, karena cara kerja platform streaming ini.

Cara Netflix Menyimpan Satu Film

Untuk satu video, katakanlah satu film bernama Movie One, ia melewati proses bernama encoding. Ia menyiapkan resolusi berbeda untuk perangkat berbeda dan kecepatan jaringan berbeda. Untuk penjelasan tingkat tinggi, katakanlah ia punya:

  • 1080p, untuk internet cepat dan perangkat kelas atas
  • 720p, untuk koneksi sedang
  • 480p, untuk koneksi lambat atau data seluler
  • dan lebih banyak format untuk codec dan perangkat berbeda

Tergantung kecepatan jaringan dan perangkat yang Anda pakai, Netflix secara dinamis mengirim versi yang dioptimalkan dari konten itu supaya Anda tidak membuang bandwidth dan beban di server Netflix juga turun. Itu semua soal encoding. Tapi pertanyaan sebenarnya: bagaimana Netflix benar-benar mengirim ratusan dan ribuan terabyte data ke jutaan pengguna yang tersebar di seluruh dunia, dengan buffering minimal?

Jawabannya: CDN (Content Delivery Network)

Netflix punya server origin-nya sendiri, katakanlah di suatu tempat di AS, di berbagai pusat data dan lokasi, dengan rak server yang menyimpan film-film aktualnya. Tapi Netflix melakukan langkah ekstra. Di seluruh dunia, di berbagai lokasi, Netflix menempatkan yang disebut Edge Location.

Mereka disebut Edge Location karena server ini ditempatkan strategis supaya latensi bagi pengguna di region itu minimal. Jika ada server di India, maka untuk pengguna India, latensi request data yang dilayani server itu minimal, dibanding semua request yang harus melewati server origin yang berada di AS.

Bayangkan apa yang terjadi tanpanya: semua orang di India yang ingin menonton film di Netflix akan mengirim request sampai ke pusat data AS. Secara geografis, itu jarak jauh bagi data untuk bepergian. Waktu respons tinggi, Anda akan mengalami buffering. Tapi dengan edge server di Mumbai atau Chennai, jarak itu runtuh mendekati nol.

Netflix CDN architecture diagram Origin Server (US) actual movies stored here Netflix US Data Center Edge / Europe PoP, cached Frankfurt/London Edge / India PoP, cached Mumbai/Chennai Edge / SE Asia PoP, cached Singapore/Tokyo Edge / Americas PoP, cached NY/LA/Sao Paulo EU users IN users SEA users US users subset of content cached at edge locations, TTL based, ML-selected
Fig 1.2: Netflix CDN: Content distributed from origin to Edge/PoP servers globally, served to nearby users

Istilah kunci dari contoh ini, kosakata penting yang akan Anda pakai sepanjang karier:

  • CDN (Content Delivery Network): Jaringan server terdistribusi global yang men-cache dan mengirimkan konten dari lokasi yang secara geografis lebih dekat ke pengguna akhir. Netflix punya CDN sendiri (disebut Open Connect). Penyedia CDN lain meliputi Cloudflare, Akamai, Fastly, AWS CloudFront.
  • Edge Location / Edge Server / Edge Computing: Kapan pun Anda melihat kata "edge" dalam konteks infrastruktur, intinya ia server yang paling dekat dengan pengguna alih-alih server terpusat atau origin. "Edge" adalah tepi jaringan, dekat dengan pengguna akhir.
  • PoP (Point of Presence): PoP cuma istilah keren yang intinya sebuah region tertentu tempat ada banyak edge server. Kumpulan banyak edge server yang terkonsentrasi di satu region disebut PoP. Misalnya, Netflix mungkin punya PoP di Mumbai yang terdiri dari 10-20 edge server.
  • Originating Server: Server master/primer tempat data kanonis sebenarnya tinggal. Edge server mengambil konten dari sini, men-cache-nya lokal, dan melayani pengguna dari salinan mereka sendiri.
  • TTL (Time to Live): Durasi yang dikonfigurasi pada konten ter-cache, setelah itu ia dianggap basi. Saat TTL kedaluwarsa, edge server harus mengambil ulang konten dari originating server untuk mendapat salinan segar. Perusahaan men-set "durasi wajar", konten ini sebaiknya hanya di-cache beberapa jam ke depan karena setelah itu mungkin ada versi baru.
Netflix's Smart Caching / Not Everything Gets Cached Everywhere

Netflix tidak men-cache semua datanya di semua edge location. Itu biaya luar biasa dan butuh banyak sumber daya. Gantinya, mereka memakai algoritma machine learning, analisis tren, data regional waktu nyata, dan banyak komputasi rumit lain untuk memutuskan subset data apa yang di-cache di tiap edge location spesifik. Server di Mumbai mungkin men-cache film Bollywood populer dan acara India yang sedang tren, sementara server di Tokyo men-cache anime dan J-drama. Ini caching cerdas berbasis data regional, bukan replikasi paksa.

CDN bukan hanya untuk streaming video. Platform seperti Vercel memakai strategi yang sama persis untuk melayani aset web statis, bundel JavaScript, file HTML, CSS, gambar, dari edge terdekat dengan pengguna yang meminta. Saat Anda deploy aplikasi Next.js di Vercel, ia otomatis didistribusikan ke jaringan edge global mereka. Itulah kenapa deployment Vercel memuat hampir seketika di mana pun Anda berada di dunia. Prinsip sama, mekanisme sama, jenis konten berbeda.

Contoh 3: Twitter / X (Trending Topics)

Mari ambil platform bernama X, dulu dikenal sebagai Twitter. Anda bisa menerapkan contoh ini ke platform media sosial mana pun, Facebook, LinkedIn, YouTube, mereka semua mengimplementasikan strategi jenis yang sama.

Jika Anda familiar dengan cara kerja Twitter, ia punya bagian bernama Trending Topics. Yang Twitter lakukan adalah mengidentifikasi topik yang sedang tren dengan menganalisis jutaan dan miliaran tweet secara waktu nyata. Ia menganalisis semua tweet yang dibuat orang di seluruh dunia, mengekstrak pola dan tren, dan menghitung apa yang sedang tren.

Kenapa Komputasi Ini Mahal

Perhitungan ini sangat mahal. Ia melibatkan:

  • Algoritma berbasis machine learning untuk natural language processing, pengelompokan topik, dan deteksi tren
  • GPU dalam jumlah besar dan infrastruktur berat
  • Memproses terabyte data, kita bicara menganalisis tweet jutaan dan miliaran orang di seluruh dunia

Apa yang Terjadi Tanpa Caching

Bayangkan jika Twitter melakukan semua perhitungan ini setiap kali pengguna pergi ke bagian trending. Jika bahkan setengah dari miliaran pengguna Twitter mencoba mengakses bagian trending, dan setiap pengguna memicu perhitungan mahal ini, server tidak sanggup menangani itu, ia akan crash dalam hitungan menit atau detik. Ada miliaran orang, dan jika setiap request memicu seluruh pipeline ML, itu mustahil dipertahankan.

Apa yang Sebenarnya Dilakukan Twitter

Untuk menghindari melakukan komputasi berat ini untuk setiap request, Twitter men-cache trending topics. Setiap beberapa menit, dan tentu kita tidak tahu algoritma eksak atau durasi eksak yang dipakai Twitter, tapi ambil estimasi kasar demi contoh ini: setiap beberapa menit, Twitter mengambil semua data ini dari berbagai region, menjalankan berbagai algoritma machine learning dan algoritma deteksi tren di tingkat yang sangat tinggi, lalu menyimpan hasilnya di in-memory key-value store seperti Redis.

Saat pengguna meminta bagian trending, alih-alih menghitungnya lagi, ia hanya mengambil data itu dari cache dan mengirimnya ke pengguna. Itulah alasan begitu Anda membuka ponsel, Anda mendapat data itu seketika, Anda tidak melihat loading time yang signifikan. Jika koneksi internet Anda umumnya cepat, seluruh interaksi UI sangat cepat.

Why Trending Topics is Safe to Cache

Untuk sebuah tren berubah di region tertentu, katakanlah negara Anda sedang ada pemilu, dan pemilu adalah topik trending, itu bukan sesuatu yang berubah dalam hitungan detik atau menit. Paling tidak ia akan bertahan di bagian trending beberapa jam atau beberapa hari. Karena data ini tidak berubah secara dinamis menit-ke-menit, sangat aman untuk di-cache. TTL bisa di-set beberapa menit atau bahkan lebih lama tanpa kehilangan kesegaran yang terasa bagi pengguna.

Polanya: Mengenali Kapan Harus Cache

Setelah tiga contoh itu, Anda sekarang harusnya melihat polanya dengan jelas. Setiap kali ada situasi yang soal:

  • Melakukan banyak komputasi berat: seperti algoritma ranking Google atau deteksi tren berbasis ML Twitter, Anda tidak ingin menghitung ulang untuk setiap request
  • Mengirim data dalam jumlah besar ke banyak pengguna: seperti Netflix mengirim terabyte konten video secara global, Anda tidak ingin selalu melayani dari satu lokasi terpusat

Inilah dua skenario umum saat caching ikut bermain. Kenali salah satunya di diskusi desain sistem, dan caching hampir selalu bagian dari solusinya.


03 / TAXONOMY

Tingkat Caching

Caching ada di banyak tingkat sebuah sistem komputer. Sebagai engineer backend, Anda akan menjumpai tiga tingkat paling sering:

Three levels of caching 1. Network Level CDN / DNS 2. Hardware Level L1 / L2 / L3 Cache / RAM 3. Software Level Redis / Memcached / ElastiCache
Fig 2.1: The three primary levels of caching a backend engineer encounters
Note on "Software-based" caching

"Berbasis perangkat lunak" tidak berarti murni perangkat lunak. Redis memakai RAM perangkat keras. Ia disebut berbasis perangkat lunak karena Anda berinteraksi dengannya lewat library atau API, sarana interaksinya perangkat lunak. Performanya tetap berasal dari perangkat keras di bawahnya (RAM).


04 / NETWORK LEVEL

Caching Tingkat Jaringan

Dua kasus pemakaian utama di lapisan jaringan yang ditangani engineer backend adalah caching CDN dan DNS.

4.1: CDN (Content Delivery Network)

Gagasan inti CDN adalah men-cache konten di server yang secara geografis lebih dekat ke pengguna akhir. Server apa pun yang ditempatkan dekat pengguna di "tepian" jaringan disebut Edge Node, Edge Server, atau Edge Computing.

Cara alur request CDN

CDN request flow step by step 1. User Browser enters URL 2. CDN DNS resolves nearest PoP 3. Edge Server PoP, nearest region Cache HIT serve directly to user in cache? Cache MISS fetch from origin server Origin Server source of truth cache + TTL TTL, Time to Live Each cached item has an expiry duration. After TTL expires -> re-fetch fresh content from origin.
Fig 3.1: Full CDN Request Flow: DNS -> PoP -> Cache Hit/Miss -> Origin

Keputusan routing CDN mempertimbangkan banyak parameter:

  • Lokasi geografis pengguna: temukan PoP terdekat
  • Kondisi jaringan: pengguna dengan koneksi lambat bisa di-route ke PoP dengan konten resolusi lebih rendah (mis. 480p alih-alih 1080p)
  • Beban server: hindari routing ke edge server yang kelebihan beban

4.2: Caching DNS

DNS (Domain Name System) menerjemahkan nama domain yang bisa dibaca manusia (seperti example.com) ke alamat IP yang dipakai browser untuk terhubung ke server. Proses resolusi ini, tanpa caching, sangat rekursif dan lambat.

Rantai Resolusi DNS (tanpa cache)

DNS resolution chain User Device types URL Recursive Resolver ISP / Google / Cloudflare Root Server 13-14 globally TLD Server .com / .in / .org Authoritative Name Server has the IP Recursive Resolver goes deep until it finds the IP example.com -> .com TLD -> example.com auth server -> IP
Fig 3.2: DNS Resolution Chain: User -> Resolver -> Root -> TLD -> Authoritative Name Server

Seluruh perjalanan rekursif ini mahal. DNS menyelesaikannya dengan banyak tingkat caching:

DNS cache hierarchy Each level is checked before proceeding to the next OS DNS Cache Windows/Mac/Linux Browser Cache Chrome, Firefox, etc. Recursive Resolver ISP / Google DNS / Cloudflare Full Resolution root -> TLD -> authoritative Level 1 Level 2 Level 3 Fallback
Fig 3.3: DNS Cache Hierarchy: four levels before a full resolution is triggered

Recursive Resolver disebut "rekursif" karena ia meng-query berbagai server secara rekursif (root -> TLD -> otoritatif) sampai menemukan jawabannya. Ia disediakan ISP Anda atau penyedia DNS publik seperti Google (8.8.8.8) atau Cloudflare (1.1.1.1).


05 / HARDWARE LEVEL

Caching Tingkat Perangkat Keras

Hirarki Cache CPU (L1, L2, L3)

CPU tidak membaca langsung dari RAM untuk setiap operasi, itu terlalu lambat. Gantinya, ia memelihara hirarki memori lebih kecil dan lebih cepat bernama level cache:

CPU to disk memory hierarchy HDD / SSD / Network RAM (main memory) L3 (shared) L2 L1 Cache Fastest / Smallest Slow Large Cheap
Fig 4.1: CPU Memory Hierarchy: Nested levels from fastest/smallest (L1) to slowest/largest (HDD)

Kenapa Larik Lebih Cepat untuk Akses Sekuensial

Konsekuensi praktis penting dari caching CPU: saat Anda mulai menelusuri larik secara sekuensial, algoritma prefetch/prediktif CPU mendeteksi pola sekuensial dan memuat seluruh larik (atau potongan besar) ke cache L1/L2 secara proaktif. Inilah kenapa for loop atas larik sangat cepat, datanya sudah di cache saat CPU membutuhkannya.

RAM vs Disk: Kenapa RAM Lebih Cepat (Fisikanya)

Ini alasan fundamental kenapa database dalam memori seperti Redis jauh lebih cepat daripada database berbasis disk seperti PostgreSQL atau MySQL. Memahami perbedaan perangkat keras sebenarnya penting, ini bukan sihir, ini fisika dan rekayasa.

Cara Kerja Penyimpanan Hard Disk

Di hard disk drive (HDD) tradisional, ada semacam kepala mekanis yang berputar mengelilingi disk. Saat Anda ingin membaca data, disk berputar, kepala bergerak ke track yang benar, dan ia secara fisik menemukan datanya. Itu operasi mekanis. Pikirkan, lengan fisik sungguhan harus bergerak ke satu lokasi di piringan yang berputar. Gerakan mekanis ini memakan waktu, milidetik, yang dalam istilah komputasi adalah keabadian.

Cara Kerja RAM

Random Access Memory pada dasarnya berbeda. Ia punya sekumpulan kapasitor dan transistor, dan lewat pemakaian sinyal listrik yang dikombinasikan dengan akses langsung berbasis alamat, ia bisa mengakses lokasi mana pun di memori dengan satu sinyal listrik. Tidak ada gerakan fisik, tidak ada seeking, tidak ada spinning. Data diakses dengan mengirim sinyal listrik ke alamat memori spesifik, dan itu terjadi mendekati kecepatan listrik itu sendiri.

Inilah juga kenapa disebut Random Access Memory, ia tidak peduli dari arah mana Anda mencoba mengakses data. Kecepatan dan waktunya hampir konstan di mana pun data berada di memori. Entah Anda mengakses alamat 0x0001 atau 0xFFFF, waktu pengambilannya pada dasarnya sama. Sifat ini disebut waktu akses O(1) dalam istilah struktur data.

Bandingkan dengan hard disk: mengakses data secara sekuensial (dari alamat 0 ke 100) cepat karena kepala tidak harus banyak bergerak. Tapi mengakses data secara acak (melompat dari alamat 0 ke 9000 ke 200 ke 7000) sangat lambat karena kepala harus mencari lokasi berbeda secara fisik tiap kali. Itulah kenapa HDD menyukai pola akses sekuensial.

Sifat RAM (Penyimpanan Primer) HDD (Penyimpanan Sekunder) SSD (Penyimpanan Sekunder)
Mekanisme akses Sinyal listrik -> alamat memori Kepala mekanis mencari piringan berputar Sel memori flash, listrik
Waktu akses ±60-100 nanodetik ±5-10 milidetik ±50-150 mikrodetik
Kecepatan relatif ±100.000x lebih cepat dari HDD Baseline ±100x lebih cepat dari HDD
Akses acak O(1), konstan di mana pun Lambat, butuh seek mekanis Bagus, tanpa bagian mekanis
Volatilitas Volatile, data hilang saat mati Persisten, bertahan saat mati Persisten, bertahan saat mati
Kapasitas (server khas) 64GB, 512GB Beberapa TB Beberapa TB
Biaya per GB ±$5-10/GB ±$0.02-0.05/GB ±$0.10-0.20/GB

Kompromi Fundamental

Soal Random Access Memory, kita menukar non-volatilitas dan kapasitas demi kecepatan. Itu komprominya dinyatakan polos. RAM sangat cepat tapi:

  • Ia volatile. Kapan pun Anda mematikan daya, apa pun data yang tersimpan di RAM, ia hilang. Ia membersihkan diri. Mulai segar kapan pun Anda menyalakan komputer. Ini karena cara kerja kapasitor, mereka butuh pasokan listrik konstan untuk memegang state-nya. Potong daya, state-nya menghilang.
  • Kapasitasnya terbatas. Meski kecepatan ada di tangan kita dengan RAM, kita tidak memilikinya berlimpah. Biaya manufaktur RAM di skala berarti server punya RAM jauh lebih sedikit daripada penyimpanan disk.

Inilah kenapa Anda tidak bisa sepenuhnya mengganti hard disk atau penyimpanan berbasis disk tradisional dengan RAM. Mereka punya peran sendiri, cepat soal akses dan pengambilan data, tapi bukan pengganti penyimpanan sekunder. Menyimpan data di penyimpanan sekunder itu permanen, tidak volatile. Tidak peduli apakah program Anda mengaksesnya atau tidak, apakah komputer Anda menyala atau mati, data tetap ada di sana karena ia secara fisik menuliskan data ke disk.

Primary vs Secondary Storage / The Summary

Penyimpanan primer (RAM): Akses data sangat cepat, kapasitas terbatas, volatile (data hilang saat mati). Dipakai untuk data yang perlu diproses sekarang atau diambil seketika. Penyimpanan sekunder (HDD/SSD): Akses data lebih lambat, kapasitas berlimpah, non-volatile (data bertahan). Dipakai untuk penyimpanan permanen semua data Anda.

Cara Redis Menjembatani Kedua Dunia

Teknologi seperti Redis dan Memcached memakai Random Access Memory ini (memori primer / memori utama) untuk menyimpan datanya, itulah kenapa operasi akses data dari database ini sangat cepat. Tapi bagaimana dengan persistensi? Bagaimana dengan fakta RAM kehilangan data saat mati?

Di balik layar, untuk persistensi, teknologi ini juga memakai penyimpanan sekunder. Dengan semacam mekanisme, saat program mulai, ia mengambil data dari penyimpanan sekunder dan memuatnya ke memori utama lagi, jadi Anda punya persistensi data. Tapi saat Anda benar-benar mengambil atau memodifikasi data, itu terjadi dengan memori primer. Database dalam memori bertanggung jawab mengimplementasikan lapisan persistensi ini, entah snapshot RDB Redis atau log AOF (Append-Only File)-nya.

MDN Reference
MDN, Cache Glossary

06 / IN-MEMORY DATABASES

Database NoSQL Key-Value Dalam Memori

Kembali ke konteks pengembangan backend, teknologi seperti Redis, Memcached, dan jika kita bicara teknologi Cloud maka AWS ElastiCache: inilah yang ikut bermain. Mereka menyediakan semacam penyimpanan, dan penyimpanan itu berbasis memori primer (RAM). Itulah alasan operasi akses data dari database ini sangat cepat.

Kita menyebut teknologi ini database NoSQL key-value dalam memori. Nama itu punya empat bagian, dan masing-masing memberi tahu Anda satu hal penting:

Dalam Memori

Dibanding database tradisional seperti PostgreSQL atau MySQL, ini tidak disimpan di disk. Penyimpanannya berbasis RAM (penyimpanan primer). Itulah alasan operasi akses data sangat cepat, kita baru saja memahami di bagian perangkat keras persis kenapa RAM lebih cepat berlipat ganda dari akses berbasis disk. Redis membaca dan menulis ke RAM, bukan disk.

Key-Value

Dibanding database relasional tradisional yang punya skema sangat ketat, Anda harus membuat tabel, membuat baris, mendefinisikan kolom dengan tipe, dll., di sini struktur datanya sangat sederhana. Anda punya key dan value. Anda punya satu key tertentu, dan untuk key itu Anda bisa menyimpan apa pun, bisa larik, objek JSON, string, angka, hash, set. Teknologi berbeda menawarkan tipe data berbeda, tapi antarmukanya selalu: beri aku key, kuberi nilainya.

NoSQL

Mereka tidak menegakkan keketatan database SQL tradisional. Tanpa skema, tanpa join, tanpa query rumit. API-nya sengaja sederhana. Di Redis, Anda intinya punya SET key value dan GET key sebagai operasi utama. Tidak rumit seperti query SQL dengan agregasi, GROUP BY, dll., aksesnya cukup langsung.

Database

Meski "cuma" key-value store, ini database kelas penuh dengan fitur seperti kedaluwarsa berbasis TTL, opsi persistensi, pesan pub/sub, skrip Lua (Redis), clustering dan replikasi. Label "database" memang layak, mereka mengelola data dengan andal, bukan sekadar cache sembarang.

Kenapa Kesederhanaan Key-Value adalah Fitur, Bukan Keterbatasan

Anda mungkin bertanya: kenapa memakai database tanpa query rumit? Jawabannya: semua kompleksitas yang tidak Anda miliki adalah performa yang Anda dapatkan kembali. Saat Redis menerima perintah GET, ia benar-benar hanya mencari hash table di memori. Tanpa parsing query, tanpa perencanaan query, tanpa I/O disk, tanpa penelusuran index. Itu pencarian memori O(1), itulah kenapa Redis bisa melayani jutaan operasi per detik.

Inilah yang akan Anda tangani sebagai engineer backend: Anda ambil library kompatibel apa pun yang tersedia di bahasa pemrograman Anda (Node.js punya node-redis atau ioredis, Go punya go-redis, Python punya redis-py), dan tergantung itu Anda tinggal pakai library-nya. Anda beri key, beri value, menyimpannya. Dan saat ingin mengambil, beri key dan dapat value. Cukup langsung, tanpa kerumitan seperti query SQL dan agregasi. Tapi semua keakraban teknis dengan cara kerja teknologi di balik layar, apa komponen utamanya, membantu Anda memahami keseluruhannya dan membuat keputusan lebih baik.

Cara Redis Menangani Persistensi

Redis memakai dua mekanisme utama untuk memastikan data tidak hilang saat proses restart:

  • RDB (Redis Database) Snapshot: Pada interval terkonfigurasi (mis. setiap 5 menit, atau saat N tulisan terjadi), Redis fork proses dan menulis seluruh dataset dalam memori ke file biner .rdb di disk. Saat startup, Redis memuat file ini kembali ke memori. Restart cepat, tapi Anda bisa kehilangan sampai N menit data.
  • AOF (Append-Only File): Setiap operasi tulis dicatat ke file append-only di disk. Saat restart, Redis memutar ulang semua operasi ini untuk membangun ulang state dalam memori. Kehilangan data hampir nol, tapi restart lebih lambat dan file tumbuh lebih besar seiring waktu (Redis memadatkannya berkala lewat proses rewrite latar).
  • Keduanya bersama: Redis merekomendasikan memakai RDB + AOF di produksi untuk keseimbangan terbaik antara performa dan daya tahan.

Wawasan kuncinya: data selalu dibaca dari dan ditulis ke RAM selama operasi normal. Disk hanya terlibat untuk persistensi (menyimpan state untuk pemulihan). Jalur panas, jalur yang diambil setiap request pengguna, tidak pernah menyentuh disk.


07 / STRATEGIES

Strategi Caching

Ada dua strategi caching utama yang akan Anda temui di pengembangan backend sehari-hari. Keduanya menjawab pertanyaan berbeda: kapan Anda mengisi cache? dan kapan Anda memperbaruinya?

Strategy 1

Lazy Caching (Cache-Aside)

Cache hanya diisi saat data pertama kali diminta. Pengisian awal proaktif tidak dilakukan.

Strategy 2

Write-Through Caching

Setiap tulis ke database sekaligus ditulis ke cache. Cache selalu segar.

Strategi 1: Lazy Caching (Cache-Aside)

Lazy caching flow diagram Client Server checks cache HIT -> return instantly Cache MISS -> fetch, store, return DB Store in cache result returned; cache populated for future requests
Fig 5.1: Lazy (Cache-Aside) Caching: populate on first miss

Karakteristik Lazy Caching:

  • Cache hanya diisi saat data benar-benar diminta, tanpa pengisian awal yang terbuang
  • Request pertama untuk data apa pun selalu menghasilkan cache miss (latensi sedikit lebih tinggi untuk pengguna pertama)
  • Risiko data basi, jika DB di bawahnya berubah, nilai ter-cache bisa usang sampai TTL kedaluwarsa atau invalidasi manual
  • Sederhana diimplementasikan dan sangat umum dalam praktik

Strategi 2: Write-Through Caching

Setiap kali operasi tulis (POST, PUT, PATCH) mengubah data di database, perubahan yang sama sekaligus diterapkan ke cache dalam alur eksekusi panggilan API yang sama.

Advantage

Cache selalu segar. Anda tidak pernah menyajikan data basi karena cache diperbarui di waktu yang sama persis dengan database.

Tradeoff

Setiap operasi tulis membawa overhead tambahan: Anda harus memperbarui database dan cache secara atomik (atau hampir atomik). Ini menambah latensi operasi tulis. Jika operasi tulis sangat sering, ini bisa menjadi bottleneck.

Kapan memakai yang mana?

SkenarioStrategi
Read-heavy, tulis jarang (halaman produk, profil)Lazy caching + TTL
Butuh cache selalu segar (data finansial, inventaris)Write-through
Pola akses tidak diketahui, peluncuran bertahapLazy caching (mulai lebih aman)
Beban kerja tulis berat (logging, event stream)Hindari write-through

08 / EVICTION POLICIES

Kebijakan Eviction

Hal lain yang harus kita sadari saat bekerja dengan cache dalam memori seperti Redis adalah kebijakan eviction. Apa artinya? Mari pahami masalahnya dulu.

Kenapa Eviction Perlu

Saat Anda punya cache, dan seperti sudah kita tahu, cache dalam memori seperti Redis memakai penyimpanan primer (RAM), yang kapasitasnya terbatas dibanding penyimpanan sekunder, sudah jelas bahwa pada satu titik Anda akan kehabisan memori. Entah Anda menjalankan Redis di server sendiri dan RAM penuh, atau memakai layanan terkelola seperti AWS ElastiCache yang punya batas penyimpanan, pada satu titik Anda akan kena plafon.

Pada titik itu, Anda harus memutuskan: Anda ingin menyimpan data baru di cache, tapi tidak ada ruang. Anda harus menghapus sesuatu yang lama untuk memberi ruang bagi yang baru. Dan tentu, seperti sudah dibahas dari bagian awal topik ini, cache hanyalah subset dari data, data yang sering diakses disimpan di lokasi berbeda yang lebih cepat. Kata kuncinya: subset dari penyimpanan primer. Kita tidak bisa menyimpan semua penyimpanan primer di cache. Jadi kita harus memutuskan apa yang tinggal dan apa yang pergi.

The Core Question of Eviction

Potongan data ter-cache mana yang paling tidak berharga untuk disimpan? Evict itu, beri ruang bagi data baru yang prioritasnya lebih tinggi. Kebijakan eviction berbeda menjawab pertanyaan ini secara berbeda.

  • NO EVICTION. Tidak ada kebijakan eviction yang dikonfigurasi. Ini default jika Anda belum mengonfigurasi kebijakan eviction apa pun. Apa yang terjadi saat cache penuh dan Anda mencoba menyisipkan data baru? Anda cuma dapat error: memori penuh, operasi ditolak. Ini tidak masuk akal untuk kasus pemakaian cache, tapi ia ada sebagai konfigurasi karena ada situasi tempat Anda secara eksplisit ingin mengendalikan ukuran cache dan jangan pernah diam-diam membuang data. Untuk kasus caching sungguhan, Anda harus selalu mengonfigurasi salah satu kebijakan di bawah.
  • LRU Least Recently Used. Algoritmanya memeriksa data mana yang paling jarang diakses baru-baru ini. Ia melacak kapan terakhir kali sebuah key tertentu diakses. Contoh berjalan dari kuliah: Anggap kita punya 4 key di cache, Key 1, Key 2, Key 3, Key 4, dan memori sekarang penuh. Database melacak kapan tiap key terakhir diakses: Key 1, diakses hari ini Key 2, diakses hari ini Key 3, diakses hari ini Key 4, diakses kemarin Sekarang Key 5 ingin masuk cache. Dengan kebijakan eviction LRU, Redis memeriksa data mana yang paling lama diakses. Karena Key 4 diakses kemarin dan semua yang lain hari ini, ia memilih Key 4: "kamu keluar, Key 5 masuk." Itulah eviction Least Recently Used. Kapan memakai: LRU kebijakan eviction yang paling umum dipakai. Ia bekerja baik untuk beban kerja tempat data yang baru diakses kemungkinan besar diakses lagi segera, yang berlaku untuk kebanyakan aplikasi dunia nyata (temporal locality).
  • LFU Least Frequently Used. Alih-alih melacak kapan sebuah key terakhir diakses, LFU melacak berapa kali total setiap key diakses. Key dengan jumlah akses terendah di-evict. Contoh berjalan dari kuliah: Lupakan waktu, anggap kita punya 4 key dan Key 5 ingin masuk. Tiap key punya penghitung frekuensi akses: Key 1, diakses 5 kali sejauh ini Key 2, diakses 10 kali sejauh ini Key 3, diakses 6 kali sejauh ini Key 4, diakses 23 kali sejauh ini Key 5 tiba dan butuh ruang. LFU memeriksa key mana yang paling jarang dipakai, yang frekuensinya terendah. Key 1 baru diakses 5 kali, paling sedikit. Key lain diakses lebih sering. Jadi ia memilih Key 1: "kamu keluar, Key 5 masuk." Kapan memakai: LFU bekerja lebih baik daripada LRU saat aplikasi Anda punya set "hot" items yang stabil dan selalu populer (seperti katalog produk tempat 100 produk teratas menerima 80% lalu lintas). LRU bisa meng-evict item panas yang kebetulan tidak diakses semenit terakhir, sedangkan LFU mempertahankannya karena frekuensi jangka panjangnya tinggi.>
  • BERBASIS TTL Volatile-TTL (Eviction Berbasis Time-to-Live). Di Redis Anda bisa mengonfigurasi TTL, time-to-live, untuk key berbeda secara individual. Saat cache penuh dan perlu evict sesuatu, dengan kebijakan ini ia memeriksa key mana yang punya TTL tersisa terendah: yaitu, key mana yang toh paling cepat kedaluwarsa. Contoh berjalan: Skenario sama, 4 key, Key 5 mau masuk, cache penuh. Tiap key punya TTL: Key 1, kedaluwarsa dalam 3 jam Key 2, kedaluwarsa dalam 45 menit Key 3, kedaluwarsa dalam 2 jam Key 4, kedaluwarsa dalam 10 menit (paling cepat kedaluwarsa) Key 5 tiba. Kebijakannya memilih Key 4, ia toh akan kedaluwarsa dalam 10 menit, jadi sebaiknya evict sekarang dan beri ruang untuk Key 5. Logikanya: "key ini sebentar lagi hilang juga, jadi lepaskan sekarang." Catatan: TTL juga dipakai terpisah dari eviction untuk sekadar mengeluarkan data basi secara otomatis. Anda set key dengan SETEX weather_delhi "25 degC" 3600 dan setelah 3600 detik (1 jam), Redis otomatis menghapusnya. Anda tidak perlu membersihkannya manual. Ini salah satu fitur Redis paling berguna untuk caching respons API eksternal.nses.
LRU eviction step-by-step visualization LRU Example: Cache full (4 keys), Key 5 arrives -> evict oldest-accessed key Key 1 last access: today Key 2 last access: today Key 3 last access: today Key 4 last access: yesterday <- OLDEST ^ EVICTED Key 5 new arrival needs space ^ ENTERS Cache removes Key 4 (oldest), inserts Key 5, total remains at 4 keys
Fig 6.1: LRU Eviction: Key 4 (accessed yesterday, least recently used) evicted to make room for Key 5
Redis Eviction Policies / Official Docs
redis.io, Key eviction (noeviction, allkeys-lru, allkeys-lfu, volatile-ttl, volatile-lru, volatile-lfu, allkeys-random)

09 / USE CASES

Kasus Pemakaian Redis dalam Pengembangan Backend

Sekarang kita punya semua landasan teoretis, mari lihat kasus pemakaian konkret tempat Redis dan database dalam memori dipakai dalam alur kerja backend yang khas. Inilah situasi yang akan Anda temui di proyek nyata.

9.1: Caching Query Database

Salah satu kasus pemakaian utama. Anggap Anda punya query SQL yang punya banyak JOIN, ia mencoba join banyak tabel, melakukan banyak agregasi, dan akhirnya berakhir dengan beberapa baris. Itu operasi yang sangat intensif komputasi karena dataset Anda besar (katakanlah jutaan dan jutaan baris), dan Anda sadar lewat monitoring bahwa API tertentu yang memanggil query database ini dipukul cukup sering, mungkin halaman landing atau halaman dashboard yang dipukul banyak pengguna.

Apa yang terjadi pada database Anda tanpa caching? Setiap pengguna yang memukul halaman itu memicu JOIN multi-tabel mahal yang sama. Dengan seribu pengguna serentak, Anda membuat seribu query mahal identik ke database. Ini menaruh beban luar biasa di server DB dan menaikkan latensi respons API untuk semua orang.

Yang Anda lakukan dengan caching: Anda ambil hasil query tertentu itu, cache dengan TTL (katakanlah 1 jam), dan sejak saat itu, saat request berikutnya datang, Anda periksa apakah hasilnya ada di cache. Jika ya, sajikan dari sana. Kalau tidak, lakukan perhitungan sekali, simpan di cache, dan kembalikan. Kapan pun ada modifikasi pada data di bawahnya, Anda bisa meng-invalidate cache secara manual atau menghapusnya, dan request berikutnya akan menghitung ulang dengan segar.

Contoh Halaman Produk Amazon

Ini salah satu ilustrasi dunia nyata terbaik. Bayangkan ada obral MacBook di Amazon. Jika Amazon tidak men-cache detail produk MacBook itu, maka selama masa obral, jutaan pengguna akan memukul halaman web itu. Mengambil gambar MacBook, semua deskripsi produk, spesifikasinya, review-nya, itu semua operasi database.

Dengan jutaan pengguna serentak di obral itu, database akan menerima sejuta request identik untuk data yang sama persis, dan itu menaruh beban signifikan di database tanpa alasan sama sekali, karena informasi seperti detail produk MacBook tidak sering berubah. Deskripsi produk, gambar, spesifikasi, itu data statis. Mungkin berubah sebulan sekali, atau bahkan tidak. Itu membuatnya kandidat sangat baik untuk caching.

Jadi Amazon men-cache data statis seperti detail produk dan harga supaya mereka bisa mengurangi beban di database, dan database bisa benar-benar melakukan kerja penting, seperti menangani transaksi checkout, pembaruan inventaris, dan manajemen order, alih-alih menghabiskan seluruh kapasitasnya melayani halaman detail produk statis yang sama berulang-ulang.

Caching Profil Media Sosial

Platform media sosial seperti Twitter dan Facebook juga men-cache data profil pengguna. Pikirkan, data profil pengguna bukan sesuatu yang sering berubah. Mungkin beberapa kali setahun. Itulah alasan mereka men-cache data profil pengguna supaya setiap kali data itu diambil, ia disajikan dari cache alih-alih dari database.

Sekarang bayangkan jika itu profil media sosial seorang selebriti. Halaman tertentu dan API tertentu untuk mengambil detail profil pengguna selebriti itu mungkin dipukul seribu kali sehari secara normal, atau jika mereka punya film yang akan datang, mungkin sejuta kali sehari. Dalam kasus itu, menaruh semua beban itu di database sama sekali tidak masuk akal, karena informasi profil pengguna itu cukup statis sebagian besar waktu. Ia bisa menyajikan konten itu dari cache, dan bahkan jika pengguna membuat perubahan pada profilnya, Anda bisa meng-invalidate cache dan menaruh entri baru. Ini operasi yang sangat read-heavy dengan tulis yang sangat jarang, skenario ideal untuk caching.

The Read-Heavy Pattern

Kapan pun kita punya operasi read-heavy dan tulisnya cukup jarang, kita bisa memakai caching. Caching query database salah satu contoh utama kapan kita memakai teknologi seperti Redis atau database dalam memori. Polanya: baca sering, data jarang berubah, komputasi mahal -> cache.

9.2: Penyimpanan Session Token

Jika Anda sudah menonton video autentikasi di daftar putar ini, Anda mungkin tahu ini. Dalam alur autentikasi khas, setelah autentikasi berhasil, session token dibuat untuk pengguna itu dan session token itu disimpan di semacam penyimpanan.

Idealnya, ia disimpan di Redis atau database dalam memori, bukan di database relasional utama Anda. Begini alasannya:

Setiap kali pengguna membuat request atau panggilan API ke endpoint apa pun di server Anda, Anda harus memvalidasi session token itu: ambil informasi sesinya dan periksa apakah valid. Jika Anda tidak memakai Redis, Anda harus mengambilnya dari database untuk setiap panggilan API. Dan seperti yang sudah Anda tahu, mengambil data dari RAM (Redis) jauh jauh lebih cepat daripada mengambil dari database.

Anggap apa yang terjadi di skala: jika Anda punya 100.000 pengguna aktif serentak, masing-masing membuat beberapa panggilan API per menit, itu berpotensi jutaan query database per menit hanya untuk validasi sesi, query yang mengembalikan data yang sama persis (sesi valid, user ID X) untuk session token yang sama berulang-ulang. Ini menaruh beban tak perlu di database Anda dan menambah latensi ke setiap endpoint API di aplikasi Anda.

Dengan Redis: session token adalah key-nya, data sesi pengguna (user ID, izin, metadata) adalah value-nya. Validasi adalah satu GET session:token_id, pencarian RAM O(1) yang memakan nanodetik. Sesi juga kedaluwarsa alami lewat TTL, jadi Anda tidak butuh pekerjaan pembersihan.

9.3: Caching Respons API Eksternal

Di backend Anda, Anda memakai semacam API eksternal, katakanlah API cuaca, dan Anda mengambil informasinya lalu melakukan semacam komputasi untuk melayani frontend Anda sendiri. Sekarang setiap kali frontend Anda membuat request ke API Anda, jika Anda tidak memakai caching, Anda juga membuat request lain ke API cuaca untuk mengambil data cuacanya.

Jika Anda punya banyak pengguna dan mereka membuat banyak panggilan API, Anda berakhir membuat ribuan panggilan API ke API eksternal ini. API eksternal biasanya punya:

Dalam kasus ini data cuaca bukan data waktu nyata dalam arti ia berubah setiap detik. Data cuaca tidak berubah setiap detik atau menit, sebab ituia jenis data yang aman di-cache. Yang Anda lakukan: ambil informasi itu dari API cuaca, cache di Redis dengan TTL 1 jam, dan untuk 1 jam berikutnya semua request dari frontend Anda akan memakai data cuaca ter-cache. Setelah satu jam, cache otomatis ter-invalidate, dan lain kali request datang Anda akan mengambil data cuaca segar, menaruhnya kembali di cache dengan TTL baru, dan mengembalikannya. Untuk jam berikutnya, semua request memakai versi ter-cache segar itu lagi.

When External API Caching is Appropriate

Tanyakan pada diri sendiri: seberapa sering data ini benar-benar berubah dengan cara yang bermakna? Cuaca -> per jam. Kurs -> setiap beberapa menit. Harga saham -> setiap detik (terlalu volatil untuk di-cache). Judul berita -> per jam. Jawabannya menentukan TTL Anda. Jika data berubah lebih lambat daripada laju lalu lintas Anda, cache.

9.4: Rate Limiting

Satu kasus pemakaian terakhir yang terlintas, karena kita sedang membahas rate limiting, mekanisme rate limiting juga kebanyakan diimplementasikan memakai teknologi seperti Redis atau cache dalam memori apa pun.

Cara rate limiting diimplementasikan: ia biasanya semacam middleware yang duduk di suatu tempat di tengah pipeline request, sebab itu disebut middleware. Sebelum request diteruskan ke route atau controller Anda, ia melewati middleware rate limit ini lebih dulu.

Cara Kerja Middleware Rate Limiting Langkah demi Langkah

Middleware mengambil sebuah header dari request yang masuk, semacam header yang memberi alamat IP pengguna. Biasanya headernya seperti X-Forwarded-For. Header ini kebanyakan dipakai untuk mengimplementasikan rate limiting guna menemukan alamat IP publik klien dari mana pun request itu datang. Ini biasanya ditambahkan reverse proxy seperti Nginx atau apa pun yang Anda pakai.

Tugas middleware ini:

  1. Ekstrak header X-Forwarded-For dari request yang masuk untuk mendapat alamat IP klien
  2. Periksa Redis untuk penghitung yang terkait dengan IP itu untuk jendela waktu saat ini (mis. per menit)
  3. Naikkan penghitung itu 1
  4. Jika penghitung melebihi batas terkonfigurasi (mis. 50 request per menit), blokir request dan kembalikan status HTTP 429 Too Many Requests
  5. Jika di bawah batas, teruskan request ke handler route yang sebenarnya

Anggap kondisinya: sebuah klien tertentu hanya boleh membuat 50 request dalam 1 menit. Maka setiap kali request datang:

TTL pada key di-set 1 menit. Setelah 1 menit, key otomatis kedaluwarsa, penghitung reset, dan klien bisa membuat request lagi. Ini jendela otomatis yang bersih dengan nol kode pembersihan.

Rate limiting middleware flow with counter walkthrough Client IP: 10.0.0.1 Rate Limit Middleware reads X-Forwarded-For checks + incr counter Redis Counter "rate:10.0.0.1:<minute>" -> 37 TTL: 60s (auto-resets each minute) count <= 50 -> PASS count > 50 -> 429 HTTP 429: Too Many Requests
Fig 7.1: Rate Limiting: middleware reads IP -> increments Redis counter -> blocks at 51st request

Kenapa Redis, Bukan Database Relasional, untuk Rate Limiting

Ini pertanyaan yang wajar. Anda bisa menyimpan penghitung di PostgreSQL atau MySQL, ia punya penyimpanan persisten dan kita bisa mengambil data. Itu mungkin. Tapi bedanya: mengambil data dari database relasional memakan lebih banyak waktu. Bahkan selisih 20 atau 30 milidetik berdampak signifikan pada latensi API, karena rate limiting berjalan di setiap request.

Jika kita menyimpannya di database relasional, maka untuk tiap request kita akan membuat panggilan database. Pada gilirannya, pertama, latensi API itu akan naik karena kita membuat panggilan database tak perlu untuk tiap request. Kedua, beban di database kita juga naik, katakanlah ada seribu pengguna membuat 100 request per menit. Itu 100.000 query database per menit hanya untuk kenaikan penghitung rate limiting. Database Anda akan kebanjiran hanya dengan overhead lapisan rate limiting.

Itulah alasan kita ingin memisahkan ini, karena dua alasan. Pertama, membuatnya secepat mungkin supaya kita bisa meminimalkan latensi API. Kedua, menurunkan beban database. Itulah alasan kapan pun kita bicara mengimplementasikan rate limiting, kita memakai database dalam memori seperti Redis alih-alih menyimpannya di database relasional. Perintah INCR atomik Redis sangat cocok: ia menaikkan key 1 dalam satu operasi atomik, tanpa race condition, tanpa butuh transaksi atau lock.


10 / CODE

Contoh Kode

Di bawah ini implementasi praktis pola caching memakai Go dan Python, dua bahasa yang dirujuk di kursus ini.

10.1: Lazy (Cache-Aside) Caching di Go

cache_aside.go / Go + github.com/redis/go-redis/v9
package main

import (
    "context"
    "encoding/json"
    "fmt"
    "time"

    "github.com/redis/go-redis/v9"
)

type Product struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Price float64 `json:"price"`
}

var rdb = redis.NewClient(&redis.Options{
    Addr: "localhost:6379",
})

// GetProduct implements Cache-Aside (Lazy) caching.
// 1. Check Redis first
// 2. On miss, fetch from DB, store in cache, return
func GetProduct(ctx context.Context, productID string) (*Product, error) {
    cacheKey := "product:" + productID

    // Step 1: Try cache first (Cache Hit path)
    cached, err := rdb.Get(ctx, cacheKey).Result()
    if err == nil {
        var product Product
        json.Unmarshal([]byte(cached), &product)
        fmt.Println("[CACHE HIT]", productID)
        return &product, nil
    }

    // Step 2: Cache Miss, fetch from database (expensive operation)
    fmt.Println("[CACHE MISS] fetching from DB...", productID)
    product, err := fetchFromDatabase(productID) // simulate DB call
    if err != nil {
        return nil, err
    }

    // Step 3: Store in cache with a 1-hour TTL
    data, _ := json.Marshal(product)
    rdb.Set(ctx, cacheKey, data, 1*time.Hour)

    return product, nil
}

// Write-Through: update DB and cache simultaneously
func UpdateProduct(ctx context.Context, product *Product) error {
    // Step 1: Update in database
    if err := updateInDatabase(product); err != nil {
        return err
    }

    // Step 2: Write-through, update cache immediately
    cacheKey := "product:" + product.ID
    data, _ := json.Marshal(product)
    rdb.Set(ctx, cacheKey, data, 1*time.Hour)

    fmt.Println("[WRITE-THROUGH] DB + cache updated for", product.ID)
    return nil
}

10.2: Middleware Rate Limiting di Go

rate_limiter.go / Go + go-redis
package middleware

import (
    "context"
    "fmt"
    "net/http"
    "time"

    "github.com/redis/go-redis/v9"
)

const (
    maxRequests = 50
    windowTime  = 1 * time.Minute
)

func RateLimitMiddleware(rdb *redis.Client) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ctx := context.Background()

        // Extract client IP from X-Forwarded-For header
        // (set by reverse proxy like Nginx or Caddy)
        clientIP := r.Header.Get("X-Forwarded-For")
        if clientIP == "" {
            clientIP = r.RemoteAddr
        }

        // Redis key: per IP, per minute window
        key := fmt.Sprintf("rate_limit:%s:%d", clientIP, time.Now().Unix()/60)

        // INCR is atomic, no race condition even with concurrent requests
        count, err := rdb.Incr(ctx, key).Result()
        if err != nil {
            http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            return
        }

        // Set TTL on first request of this window (key is new)
        if count == 1 {
            rdb.Expire(ctx, key, windowTime)
        }

        // Check if limit exceeded
        if count > maxRequests {
            w.Header().Set("Retry-After", "60")
            http.Error(w, "429 Too Many Requests", http.StatusTooManyRequests)
            return
        }

        // Proceed to actual handler
        w.Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", maxRequests-count))
    }
}

10.3: Decorator Caching di Python

cache.py / Python + redis-py + FastAPI
import json
import functools
import redis
from fastapi import FastAPI

app = FastAPI()

# Connect to Redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)

def cache_result(ttl: int = 3600):
    """
    Decorator that implements lazy (cache-aside) caching.
    ttl: time-to-live in seconds (default 1 hour)
    """
    def decorator(func):
        @functools.wraps(func)
        async def wrapper(*args, **kwargs):
            # Build cache key from function name + args
            cache_key = f"{func.__name__}:{args}:{kwargs}"

            # Step 1: Check cache (Cache Hit)
            cached = r.get(cache_key)
            if cached:
                print(f"[CACHE HIT] {cache_key}")
                return json.loads(cached)

            # Step 2: Cache Miss, execute the actual function
            print(f"[CACHE MISS] calling {func.__name__}...")
            result = await func(*args, **kwargs)

            # Step 3: Store in Redis with TTL
            r.setex(cache_key, ttl, json.dumps(result))

            return result
        return wrapper
    return decorator


# Usage: apply cache decorator to any route handler
@app.get("/products/{product_id}")
@cache_result(ttl=3600)  # cache for 1 hour
async def get_product(product_id: str):
    # Expensive DB query, only runs on cache miss
    product = await fetch_from_db(product_id)
    return product


# TTL-based API Response Caching (e.g. weather)
@app.get("/weather/{city}")
async def get_weather(city: str):
    cache_key = f"weather:{city}"

    # Check cache (TTL = 1 hour, weather doesn't change every minute)
    cached = r.get(cache_key)
    if cached:
        return {"source": "cache", "data": json.loads(cached)}

    # Miss: call external weather API (costs money / rate limited)
    weather_data = await call_weather_api(city)

    # Store for 1 hour
    r.setex(cache_key, 3600, json.dumps(weather_data))

    return {"source": "api", "data": weather_data}

10.4: Manajemen Sesi dengan Redis (Python)

session.py / Python + redis-py
import uuid
import json
import redis
from datetime import timedelta

r = redis.Redis(host="localhost", port=6379, decode_responses=True)

def create_session(user_id: str, user_data: dict) -> str:
    """Create a session after successful login. Store in Redis."""
    session_id = str(uuid.uuid4())
    session_key = f"session:{session_id}"

    # Store session data with 24-hour TTL
    # TTL ensures sessions auto-expire, no manual cleanup needed
    r.setex(
        session_key,
        timedelta(hours=24),
        json.dumps({"user_id": user_id, **user_data})
    )

    return session_id  # returned to client as cookie/token


def get_session(session_id: str) -> dict | None:
    """
    Validate session on every authenticated API request.
    Redis O(1) lookup, microseconds, not milliseconds.
    """
    session_key = f"session:{session_id}"
    data = r.get(session_key)

    if not data:
        return None  # session expired or invalid

    # Optionally: refresh TTL on activity (sliding window)
    r.expire(session_key, timedelta(hours=24))

    return json.loads(data)


def delete_session(session_id: str):
    """Logout, delete session from Redis immediately."""
    r.delete(f"session:{session_id}")

REFERENCES

Bacaan Lanjutan & Dokumentasi

Dokumentasi Resmi Redis

redis.io/docs, Official Redis Documentation Redis Data Types (String, Hash, List, Set, Sorted Set, Stream) Redis Eviction Policies (noeviction, allkeys-lru, allkeys-lfu, volatile-ttl) Redis Persistence (RDB snapshots, AOF append-only log) Redis INCR command (atomic counter, used in rate limiting) Redis SETEX command (set with TTL)

MDN Web Docs

MDN, Cache Glossary MDN, DNS Glossary MDN, Cache-Control HTTP Header MDN, HTTP 429 Too Many Requests MDN, DNS Prefetch (browser-side DNS caching)

Klien Redis Go

github.com/redis/go-redis, Official Go Redis client go-redis Guide, Getting started, commands, pipelines

Klien Redis Python

redis-py Docs, Official Python Redis client

Cloudflare & CDN

Cloudflare, What is a CDN? Cloudflare, How DNS works (excellent interactive diagrams)

BACKEND ENGINEERING FIELD MANUAL / V2 / BAB 12 / CACHING. Catatan disusun dari transkrip kuliah / contoh Go + Python / referensi MDN & Redis inline